package cn.edu.ctgu.geo;

import cn.edu.ctgu.geo.constants.GridData;
import cn.edu.ctgu.geo.type.*;
import cn.edu.ctgu.geo.util.LocationCodeUtils;

/**
 * 定义 Codec2D 类，包含各种编码方法
 */
public class Codec2D {
    /**
     * 对一个经纬度坐标编码
     *
     * @param lngLat 经纬度坐标，可以写小数形式（正负号表示方向），也可以写度分秒形式（均为正数，direction 字段表示方向）
     * @return 北斗二维网格位置码
     */
    public static String encode(LngLat lngLat) {
        return encode(lngLat, 10);
    }

    /**
     * 对一个经纬度坐标编码
     *
     * @param lngLat 经纬度坐标，可以写小数形式（正负号表示方向），也可以写度分秒形式（均为正数，direction 字段表示方向）
     * @param level  要编码到第几级，默认为 10
     * @return 北斗二维网格位置码
     */
    public static String encode(LngLat lngLat, int level) {
        if (level < 0) {
            level = 10;
        }
        double[] lngLatInSec = getSecond(lngLat);
        double lngInSec = lngLatInSec[0];
        double latInSec = lngLatInSec[1];
        // 记录第 n 级网格的定位角点经纬度
        double lngN = 0, latN = 0;
        // 存储结果
        StringBuilder resCode = new StringBuilder();
        if (Math.abs(lngLat.getLatDegree()) >= 88) {
            // 都变换到东经 0~90°
            int part;
            PoleGrid parent = new PoleGrid(true, 2 * 3600);
            if (lngInSec >= 120 * 3600) {
                lngInSec -= 120 * 3600;
                part = 2;
            } else if (lngInSec <= -120 * 3600) {
                lngInSec = -lngInSec - 60 * 3600;
                part = 2;
            } else if (lngInSec < 0) {
                lngInSec = -lngInSec;
                part = 3;
            } else {
                part = 1;
            }
            for (int i = 0; i <= level; i++) {
                NPole res = encodeNPole(lngInSec, latInSec, lngN, latN, parent, i, part);
                lngN += res.getLngNP1();
                latN += res.getLatNP1();
                resCode.append(res.getCodeN());
                parent = res.getSelf();
            }
        } else {
            for (int i = 0; i <= level; i++) {
                NPole t = encodeN(lngInSec, latInSec, lngN, latN, i);
                lngN += t.getLngNP1();
                latN += t.getLatNP1();
                resCode.append(t.getCodeN());
                // 从第二级开始，对称到东北半球计算，需要均取非负数计算
                if (i == 1) {
                    lngInSec = Math.abs(lngInSec);
                    latInSec = Math.abs(latInSec);
                }
            }
        }
        return resCode.toString();
    }

    /**
     * 以下坐标均以秒表示，且第 2 级开始所有半球均对称到东北半球处理（非负）
     *
     * @param lngInSec 位置经度
     * @param latInSec 位置纬度
     * @param lngN     该位置所在第 n 级二维北斗网格的定位角点经度
     * @param latN     该位置所在第 n 级二维北斗网格的定位角点纬度
     * @param n        第 n 级
     * @return [lngN+1, latN+1, codeN]
     */
    private static NPole encodeN(double lngInSec, double latInSec, double lngN, double latN, int n) {
        if (n == 0) {
            // 南北半球标识码
            return new NPole(0, 0, latInSec > 0 ? "N" : "S", null);
        } else if (n == 1) {
            // 根据国家基本比例尺地形图分幅和编号，按照 1:1000000 对第一级进行划分
            // 经度
            int a = (int) Math.floor(lngInSec / GridData.gridSizes1[n][0]);
            // 纬度
            int b = (int) Math.floor(Math.abs(latInSec) / GridData.gridSizes1[n][1]);
            return new NPole(
                    // a < 0 时，需要取反并 -1
                    (a >= 0 ? a : -a - 1) * GridData.gridSizes1[n][0],
                    b * GridData.gridSizes1[n][1],
                    encodeFragment(n, a + 31, b),
                    null);
        } else {
            // 公式中需要 +1，为了底下方便计算没有 +1，因为之后还要 -1
            int a = (int) Math.floor((lngInSec - lngN) / GridData.gridSizes1[n][0]);
            int b = (int) Math.floor((latInSec - latN) / GridData.gridSizes1[n][1]);
            return new NPole(
                    a * GridData.gridSizes1[n][0],
                    b * GridData.gridSizes1[n][1],
                    encodeFragment(n, a, b),
                    null);

        }
    }


    private static NPole encodeNPole(double lngInSec, double latInSec, double lngN, double latN, PoleGrid parent, int n, int part) {
        if (n < 0 || n > 10) {
            throw new IllegalArgumentException("encodeNP: 等级应该在 0~10 之间");
        }
        double lngNP1;
        double latNP1;
        String codeN = "";
        PoleGrid self;
        if (n == 0) {
            lngNP1 = 0;
            latNP1 = 0;
            codeN = latInSec > 0 ? "N" : "S";
            self = new PoleGrid(true, 2 * 3600);
        } else if (n == 1) {
            lngNP1 = 0;
            latNP1 = 88 * 3600;
            codeN = "000";
            self = new PoleGrid(true, 2 * 3600);
        } else if (n == 2) {
            // 第二级时，全都对称到东经 0~120°
            if (latInSec - latN >= parent.getLatSize() / 2) {
                codeN += "0";
                if (latInSec - latN >= parent.getLatSize() * 0.75) {
                    lngNP1 = 0;
                    latNP1 = parent.getLatSize() * 0.75;
                    codeN += "0";
                    self = new PoleGrid(true, parent.getLatSize() / 4);
                } else {
                    lngNP1 = 0;
                    latNP1 = parent.getLatSize() * 0.5;
                    codeN += String.valueOf(part);
                    self = new PoleGrid(false, parent.getLatSize() / 4, 120 * 3600.0);
                }
            } else {
                codeN += String.valueOf(part);
                int row = (int) Math.floor((latInSec - latN) / (parent.getLatSize() * 0.25));
                int col = (int) Math.floor(lngInSec / (60 * 3600));
                lngNP1 = 60 * 3600 * col;
                latNP1 = row * (parent.getLatSize() * 0.25);
                codeN += String.valueOf(col + 2 * row);
                self = new PoleGrid(false, parent.getLatSize() / 4, 60 * 3600.0);
            }
        } else {
            int gridCountLng = GridData.gridCountPole[n][0];
            int gridCountLat = GridData.gridCountPole[n][1];
            if (parent.isPoint()) {
                if (latInSec - latN >= parent.getLatSize() / gridCountLat) {
                    codeN = "0";
                    lngNP1 = 0;
                    latNP1 = (parent.getLatSize() / gridCountLat) * (gridCountLat - 1);
                    self = new PoleGrid(true, parent.getLatSize() / gridCountLat);
                } else {
                    codeN = String.valueOf(part);
                    lngNP1 = 0;
                    latNP1 = 0;
                    self = new PoleGrid(false, parent.getLatSize() / gridCountLat, 120 * 3600.0);
                }
            } else {
                double gridSizeLng = parent.getLngSize() / gridCountLng;
                double gridSizeLat = parent.getLatSize() / gridCountLat;
                int a = (int) Math.floor((lngInSec - lngN) / gridSizeLng);
                int b = (int) Math.floor((latInSec - latN) / gridSizeLat);
                lngNP1 = a * gridSizeLng;
                latNP1 = b * gridSizeLat;
                codeN = Integer.toHexString(a).toUpperCase() + Integer.toHexString(b).toUpperCase();
                self = new PoleGrid(false, parent.getLatSize() / gridCountLat, parent.getLngSize() / gridCountLng);
            }
        }
        return new NPole(lngNP1, latNP1, codeN, self);
    }

    /**
     * @param level    当前编码层级
     * @param lngCount 经度方向网格数
     * @param latCount 纬度方向网格数
     * @return 当前层级的编码片段
     */
    public static String encodeFragment(int level, int lngCount, int latCount) {
        if (level == 3 || level == 6) {
            return String.valueOf(latCount * 2 + lngCount);
        } else if (level > 1 && level <= 10) {
            return Integer.toHexString(lngCount).toUpperCase() + Integer.toHexString(latCount).toUpperCase();
        } else if (level == 1) {
            // 前置位补零
            String aS = String.format("%02d", lngCount);
            char bS = (char) (65 + latCount);
            return aS + bS;
        }
        throw new IllegalArgumentException("非法层级 level");
    }

    /**
     * 计算经纬度换算成秒
     *
     * @param lngLat 经纬度对象
     * @return [经度换算成秒, 纬度换算成秒]
     */
    public static double[] getSecond(LngLat lngLat) {
        // 处理经度部分
        double lngMinute = lngLat.getLngMinute() != null ? lngLat.getLngMinute() : 0;
        double lngSecond = lngLat.getLngSecond() != null ? lngLat.getLngSecond() : 0;
        String lngDirection = lngLat.getLngDirection() != null ? lngLat.getLngDirection().name() : "E";
        // 计算经度，换算成秒
        double lngInSec = (lngLat.getLngDegree() * 3600 + lngMinute * 60 + lngSecond) * (lngDirection.equals("W") ? -1 : 1);

        // 处理纬度部分
        double latMinute = lngLat.getLatMinute() != null ? lngLat.getLatMinute() : 0;
        double latSecond = lngLat.getLatSecond() != null ? lngLat.getLatSecond() : 0;
        String latDirection = lngLat.getLatDirection() != null ? lngLat.getLatDirection().name() : "N";
        // 计算纬度，换算成秒
        double latInSec = (lngLat.getLatDegree() * 3600 + latMinute * 60 + latSecond) * (latDirection.equals("S") ? -1 : 1);

        return new double[]{lngInSec, latInSec};
    }

    /**
     * 对北斗二维网格位置码解码
     *
     * @param code 需要解码的北斗二维网格位置码
     * @return 经纬度坐标
     */
    public static LngLat decode(String code) {
        return decode(code, new DecodeOption("dms"));
    }

    /**
     * 对北斗二维网格位置码解码
     *
     * @param code         需要解码的北斗二维网格位置码
     * @param decodeOption 解码选项，可不传
     * @return 经纬度坐标
     */
    public static LngLat decode(String code, DecodeOption decodeOption) {
        if (decodeOption == null) {
            decodeOption = new DecodeOption("dms");
        }
        // 层级
        int level = LocationCodeUtils.getCodeLevel(code);
        // 半球方向
        Direction direction = LocationCodeUtils.getDirections(code);
        // 南北半球标识
        int[] signs = LocationCodeUtils.getSigns(direction);
        int lngSign = signs[0];
        int latSign = signs[1];
        // 用于累加结果
        double lng = 0;
        double lat = 0;
        // 对 1 ~ level 级进行解码
        for (int i = 1; i <= level; i++) {
            double[] pair = decodeN(code, i);
            lng += pair[0];
            lat += pair[1];
        }
        LngLat result = new LngLat();
        // 方向
        result.setLatDirection(latSign == -1 ? LatDirection.S : LatDirection.N);
        result.setLngDirection(lngSign == -1 ? LngDirection.W : LngDirection.E);

        // 格式化输出结果
        if ("decimal".equals(decodeOption.getForm())) {
            // 用小数表示
            lng *= lngSign;
            lat *= latSign;
            result.setLngDegree(lng / 3600);
            result.setLatDegree(lat / 3600);
        } else {
            // 用度分秒表示
            // 经度
            result.setLngSecond(lng % 60);
            lng = (int) ((lng - result.getLngSecond()) / 60);
            result.setLngMinute(lng % 60);
            lng = (int) ((lng - result.getLngMinute()) / 60);
            result.setLngDegree(lng);
            // 纬度
            result.setLatSecond(lat % 60);
            lat = (int) ((lat - result.getLatSecond()) / 60);
            result.setLatMinute(lat % 60);
            lat = (int) ((lat - result.getLatMinute()) / 60);
            result.setLatDegree(lat);
        }
        return result;
    }

    /**
     * 对第n级进行解码
     *
     * @param code 北斗二维网格位置码
     * @param n    层级
     * @return 该层级的经纬度偏移量（单位秒，且非负）
     */
    private static double[] decodeN(String code, int n) {
        if (n < 1 || n > 10) {
            throw new IllegalArgumentException("层级错误");
        }
        // 获取行列号
        int[] rowCol = LocationCodeUtils.getRowAndCol(LocationCodeUtils.getCodeAtLevel(code, n), n);
        // 如果是第一级，需要特殊处理
        if (n == 1) {
            if (rowCol[0] == 0) {
                throw new IllegalArgumentException("暂不支持解码两极地区(纬度大于等于88°)编码");
            }
            rowCol[0] = rowCol[0] >= 31 ? rowCol[0] - 31 : 30 - rowCol[0];
        }
        return new double[]{rowCol[0] * GridData.gridSizes1[n][0], rowCol[1] * GridData.gridSizes1[n][1]};
    }

    public static void main(String[] args) {
        // 示例使用
        LngLatEle lngLat = new LngLatEle(116, 18.0, 45.37, LngDirection.E,
                39, 59.0, 35.38, LatDirection.N, 300);
        for (int level = 1; level < 11; ++level) {
            String result = encode(lngLat, level);
            System.out.println("level:" + level + ":" + result);
        }
        System.out.println("输入：");
        System.out.println(lngLat);
        final int level = 10;
        String code = Codec2D.encode(lngLat, level);
        System.out.println("北斗网格二维位置码：(" + level + "级):" + code);
        LngLat result = Codec2D.decode(code);
        System.out.println("解码：");
        System.out.println(result);

        String d3code = Codec3D.encode(lngLat);
        System.out.println("北斗网格三维位置码：(" + level + "级):" + d3code);
        LngLatEle resultD3Code = Codec3D.decode(d3code);
        System.out.println("解码：");
        System.out.println(resultD3Code);
    }
}
